/**
 * ClientService
 * 
 * 29/12/2013
 * 
 * Copyright GPL
 */
package com.jtmproject.bluetooth;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for
 * incoming connections, a thread for connecting with a device, and a
 * thread for performing data transmissions when connected.
 * 
 * @author Javier Tejedor
 * @thanks Google
 */
public class ClientService {

	private static final UUID MY_UUID_SECURE =
			UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");

	private final BluetoothAdapter bluetoothAdapter;
	private final Handler mHandler;
	private ConnectThread connectThread;
	private ConnectedThread connectedThread;
	private int state;

	//constants that indicate the current connection state
	//we're doing nothing
	public static final int STATE_NONE = 0;
	//now listening for incoming connections
	public static final int STATE_LISTEN = 1;
	//now initiating an outgoing connection
	public static final int STATE_CONNECTING = 2; 
	//now connected to a remote device
	public static final int STATE_CONNECTED = 3;

	/**
	 * constructor.
	 * @param handler
	 */
	public ClientService(Handler handler) {
		bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
		this.state = STATE_NONE;
		mHandler = handler;
	}

	/**
	 * set the current connection state
	 * @param 
	 */
	private synchronized void setState(int state) {
		this.state = state;

		mHandler.obtainMessage(
				Client.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
	}

	/**
	 * return the current connection state. 
	 * @return
	 */
	public synchronized int getState() {
		return state;
	}

	/**
	 * specifically start AcceptThread to begin a
	 * session in listening (server) mode.
	 * */
	public synchronized void start() {
		if (connectThread != null) {
			connectThread.cancel(); 
			connectThread = null;
		}

		if (connectedThread != null) {
			connectedThread.cancel(); 
			connectedThread = null;
		}

		setState(STATE_LISTEN);
	}

	/**
	 * start the ConnectThread to initiate a connection to a remote device.
	 * @param device
	 */
	public synchronized void connect(BluetoothDevice device) {
		if (state == STATE_CONNECTING) {
			if (connectThread != null) {
				connectThread.cancel(); 
				connectThread = null;
			}
		}

		if (connectedThread != null) {
			connectedThread.cancel(); 
			connectedThread = null;
		}


		connectThread = new ConnectThread(device);
		connectThread.start();
		setState(STATE_CONNECTING);
	}

	/**
	 * start the ConnectedThread to begin managing a Bluetooth connection
	 * @param socket
	 * @param device
	 * @param socketType
	 */
	public synchronized void connected(BluetoothSocket socket, BluetoothDevice
			device, final String socketType) {

		if (connectThread != null) {
			connectThread.cancel(); 
			connectThread = null;
		}

		if (connectedThread != null) {
			connectedThread.cancel(); 
			connectedThread = null;
		}

		connectedThread = new ConnectedThread(socket, socketType);
		connectedThread.start();

		// Send the name of the connected device
		sendNameDevice(device);

		setState(STATE_CONNECTED);
	}

	/**
	 * this sends the name of the device connected
	 * @param device
	 */
	private void sendNameDevice(BluetoothDevice device) {
		Message msg = mHandler.obtainMessage(Client.MESSAGE_DEVICE_NAME);
		Bundle bundle = new Bundle();
		bundle.putString(Client.DEVICE_NAME, device.getName());
		msg.setData(bundle);
		mHandler.sendMessage(msg);
	}

	/**
	 * stop all threads
	 */
	public synchronized void stop() {
		if (connectThread != null) {
			connectThread.cancel();
			connectThread = null;
		}

		if (connectedThread != null) {
			connectedThread.cancel();
			connectedThread = null;
		}

		setState(STATE_NONE);
	}

	/**
	 * write to the ConnectedThread in an unsynchronized manner
	 * @param out
	 */
	public void write(byte[] out) {
		ConnectedThread r;

		synchronized (this) {
			if (state != STATE_CONNECTED) return;
			r = connectedThread;
		}

		r.write(out);
	}

	/**
	 * indicate that the connection attempt failed and notify the UI Activity.
	 */
	private void connectionFailed() {

		Message msg = mHandler.obtainMessage(Client.MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(Client.TOAST, "Unable to connect device");
		msg.setData(bundle);
		mHandler.sendMessage(msg);

		//restart
		ClientService.this.start();
	}

	/**
	 * indicate that the connection was lost and notify the UI Activity.
	 */
	private void connectionLost() {

		Message msg = mHandler.obtainMessage(Client.MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(Client.TOAST, "Device connection was lost");
		msg.setData(bundle);
		mHandler.sendMessage(msg);

		//restart
		this.start();
	}


	/**
	 * This thread runs while attempting to make an outgoing connection
	 * with a device. It runs straight through; the connection either
	 * succeeds or fails.
	 * 
	 * @author Javier Tejedor
	 */
	private class ConnectThread extends Thread {
		
		private final BluetoothSocket mmSocket;
		private final BluetoothDevice mmDevice;
		private String mSocketType;

		/**
		 * constructor
		 * @param device
		 */
		public ConnectThread(BluetoothDevice device) {
			mmDevice = device;
			BluetoothSocket tmp = null;

			try {
				tmp = device.createRfcommSocketToServiceRecord(MY_UUID_SECURE);
			} catch (IOException e) {

			}

			mmSocket = tmp;
		}

		/* (non-Javadoc)
		 * @see java.lang.Thread#run()
		 */
		@Override
		public void run() {
			setName("ConnectThread" + mSocketType);

			bluetoothAdapter.cancelDiscovery();

			try {

				mmSocket.connect();

			} catch (IOException e) {

				try {

					mmSocket.close();

				} catch (IOException e2) {

				}

				connectionFailed();
				return;
			}

			synchronized (ClientService.this) {
				connectThread = null;
			}

			connected(mmSocket, mmDevice, mSocketType);

		}

		/**
		 * this cancels the connection
		 */
		public void cancel() {
			try {
				mmSocket.close();
			} catch (IOException e) {

			}
		}
	}

	/**
	 * This thread runs during a connection with a remote device.
	 * It handles all incoming and outgoing transmissions.
	 * 
	 * @author Javier Tejedor
	 */
	private class ConnectedThread extends Thread {

		private final BluetoothSocket mmSocket;
		private final InputStream mmInStream;
		private final OutputStream mmOutStream;

		/**
		 * constructor
		 * @param socket
		 * @param socketType
		 */
		public ConnectedThread(BluetoothSocket socket, String socketType) {

			mmSocket = socket;
			InputStream tmpIn = null;
			OutputStream tmpOut = null;

			try {
				tmpIn = socket.getInputStream();
				tmpOut = socket.getOutputStream();
			} catch (IOException e) {

			}

			mmInStream = tmpIn;
			mmOutStream = tmpOut;
		}

		/* (non-Javadoc)
		 * @see java.lang.Thread#run()
		 */
		@Override
		public void run() {

			byte[] buffer = new byte[1024];
			int bytes;

			while (true) {
				try {
					//read from the InputStream
					bytes = mmInStream.read(buffer);

					//send the obtained bytes to the UI Activity
					mHandler.obtainMessage(
							Client.MESSAGE_READ, bytes, -1, buffer)
							.sendToTarget();
				} catch (IOException e) {

					connectionLost();
					//restart
					ClientService.this.start();
					break;
				}
			}
		}

		/**
		 * Write to the connected OutStream.
		 * @param buffer
		 */
		public void write(byte[] buffer) {
			try {
				mmOutStream.write(buffer);

				// Share the sent message back to the UI Activity
				mHandler.obtainMessage(Client.MESSAGE_WRITE, -1, -1, buffer)
				.sendToTarget();
			} catch (IOException e) {

			}
		}

		/**
		 * this is to cancel the connection with the socket
		 */
		public void cancel() {
			try {
				mmSocket.close();
			} catch (IOException e) {

			}
		}
	}
}

